Skip to content

Redis 数据结构 ​

把一个商品放进 Redis,最省事的做法是序列化成 JSON,再用 SET 保存。问题通常出现在下一步:只想改库存,却要读改写整个 JSON;要按销量排序,必须把所有商品读出来;要统计独立访客,保存每个访客又太贵。这时需要按操作选择数据结构,而不只是按数据长什么样选择。

本文讲知识地图列出的九种结构:String、Hash、List、Set、ZSet、Bitmap、HyperLogLog、Stream、Geo。目标是知道常用命令、底层如何存储,以及为什么同一份数据换一种组织方式,内存和延迟会变化。

编码结论固定在 Redis 8.2.2。 本文使用官方源码包,在 Linux/amd64 上以 libc 分配器编译,所有标为“实测”的结果来自临时单机实例。它是可复现的教学版本,不是当前生产升级建议;其他版本、配置、分配器下的编码和字节数需要重测。文末有完整实验,不需要 Go 客户端,直接验证 Redis 服务端。

先选操作,再选类型 ​

需要做什么首选结构常用入口主要代价或限制
保存一段内容、计数、带 TTL 的结果StringSET、GET、INCR、MGET修改 JSON 中一个字段通常要重写整个值
按字段读写一个对象HashHSET、HGET、HMGET、HINCRBY不能按任意字段值自动检索其他对象
保持插入顺序、从两端取任务ListLPUSH、RPUSH、LPOP、BLMOVE没有消费者组和自动确认账本
精确去重、判断成员、做交并差SetSADD、SISMEMBER、SINTER、SCARD保存所有成员,内存随基数增长
排行榜、分数区间、到期顺序ZSetZADD、ZINCRBY、ZRANGE、ZRANK成员唯一;要明确相同分数的顺序
连续整数编号的布尔状态BitmapSETBIT、GETBIT、BITCOUNT、BITOP空间由最大编号决定,稀疏编号可能浪费巨大
只需要近似去重人数HyperLogLogPFADD、PFCOUNT、PFMERGE不能列出成员,也不能判断某个成员是否存在
保留事件、分别消费、确认重试StreamXADD、XREADGROUP、XACK、XAUTOCLAIM必须设计保留、积压和重复处理策略
找附近的点GeoGEOADD、GEOSEARCH、GEODIST不是道路导航或任意几何数据库

这些是数据建模入口,不是“某结构永远最快”的排名。比如 Set 和 HyperLogLog 都能回答人数,但只有 Set 能回答“具体是谁”,因此不能只拿二者内存大小做替代决定。数据类型总览

类型是接口,编码是实现 ​

TYPE key 回答客户端能对它做哪些操作;OBJECT ENCODING key 回答此时内部怎么存。一个 Hash 从紧凑数组变成哈希表,HGET 的接口不会变。

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    K["键 user:7"] --> E["键空间中的对象"]
    E --> T["逻辑类型 Hash"]
    E --> V["当前编码 listpack"]
    V --> P["连续存储 field,value,field,value"]
    V -->|"字段太多或字段/值太长"| H["转换为 hashtable"]
    H --> D["按字段哈希定位 value"]
    T -. "HGET/HSET 接口保持一致" .-> D
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    K["键 user:7"] --> E["键空间中的对象"]
    E --> T["逻辑类型 Hash"]
    E --> V["当前编码 listpack"]
    V --> P["连续存储 field,value,field,value"]
    V -->|"字段太多或字段/值太长"| H["转换为 hashtable"]
    H --> D["按字段哈希定位 value"]
    T -. "HGET/HSET 接口保持一致" .-> D

listpack 把许多小元素紧凑放在连续内存里,少了大量独立分配和指针。代价是查找可能要扫描、插入可能要移动后面的字节。哈希表、跳表和链式节点增加管理开销,却避免大集合一直做线性扫描。因此 Redis 在小对象上选紧凑编码,超过阈值再转换。这是 CPU 与内存的取舍,不是压缩得越紧越好。8.2.2 listpack 实现

在本次默认配置下,主要边界如下。表中只列本文涉及的内建结构;带字段过期的 Hash 另有 listpackex,不能拿普通 Hash 的两种编码概括全部情况。

类型Redis 8.2.2 主要编码本文要记住的转换条件
Stringint、embstr、raw是否能表示为整数;短字符串还受键与对象布局影响
Hashlistpack、hashtable;字段过期可用 listpackex普通 Hash 默认最多 512 个字段,字段名和值各不超过 64 字节
Listlistpack、quicklist小列表可单个 listpack;超过节点限制转 quicklist,默认 list-max-listpack-size -2 对应约 8 KiB 的常规节点目标
Setintset、listpack、hashtable纯整数 intset 默认上限 512;小字符串 listpack 默认上限 128 个且每个最多 64 字节
ZSetlistpack、skiplist默认最多 128 个成员且成员最多 64 字节时可紧凑存储
BitmapString 的字符串存储按位操作改变 String 中的字节
HyperLogLog外层 String;内部稀疏或稠密 HLL 表示内部格式与 OBJECT ENCODING 报告的外层编码是两层
Streamstream,内部 radix tree 与 listpack消息块、消费者组和待确认记录分别有管理成本
GeoZSet 的 listpack 或 skiplist坐标编码为分数,成员为位置名

阈值来自 8.2.2 redis.conf,转换逻辑来自各类型源码。不要推导“删到阈值以下一定恢复紧凑编码”;转换方向和触发时机属于具体实现,应检查实际对象。CONFIG GET 能看到当前阈值,OBJECT ENCODING 能验证当前结果。

String:看起来只有一个值,内部仍有三种存法 ​

String 保存二进制安全的字节序列,可以是文本、图片片段、序列化对象或整数文本。GET 不理解 JSON;INCR 则会把值解释为可增减的整数,并在同一条命令中完成读改写。自己先 GET 再 SET 会把原子操作拆开,两个客户端可能覆盖对方的更新。String 命令实现

int 直接在对象里表示整数,embstr 把对象和短字符串放在一块分配里,raw 使用单独的动态字符串 SDS。SDS 记录长度,所以中间可以含 \0,读取长度也不需要扫描到终止符。object.c、sds.h

很多资料把 String 编码总结成“44 字节以内 embstr”。对本次版本的键空间对象,这句话不够准确。实验先写 42、44 个 x、45 个 x,然后查看编码:

python
call('SET', 'lab08:n', '42')
call('SET', 'lab08:s44', 'x' * 44)
call('SET', 'lab08:s45', 'x' * 45)
print('string:', encoding('n'), encoding('s44'), encoding('s45'))

实际输出:

text
string: int raw raw

44 字节那项也是 raw。8.2.2 的 createStringObject 确实有 44 字节限制,但进入键空间时还经过 kvobjSet:它把键也嵌入对象,并检查整体大小是否能放进一个 64 字节缓存行。只看值的创建函数,漏掉后续存入键空间的步骤,就会得出错误结论。

换成只有一个字节的键,不设过期,实测如下:

text
string-boundary: a 40 False embstr
string-boundary: b 41 False raw

这两个数字对应本文的创建路径和机器布局,不应成为新的跨版本口诀。键长、过期元数据、分配器可用空间和修改路径都值得核对。APPEND 也可能让原来的紧凑对象换成可修改表示;判断应以 OBJECT ENCODING 与固定版本代码为准。8.2.2 kvobjSet

Hash:适合局部字段更新,别把“一个 key”当成小对象 ​

HSET user:7 name Ada age 37 把两个字段放在一个 Hash。HGET user:7 name 只取一个字段,HINCRBY user:7 visits 1 原子增量更新一个整数属性,避免整段 JSON 的传输和读改写冲突。它没有二级索引:想找所有 age=37 的用户,需要另建索引或交给有查询能力的数据系统。

小 Hash 的 listpack 按字段和值成对存放;大 Hash 用哈希表。下面的实验让变化发生在眼前:

python
call('HSET', 'lab08:h', 'name', 'Ada', 'age', '37')
print('hash:', encoding('h'))
call('HSET', 'lab08:h', 'bio', 'x' * 65)
print('hash-long:', encoding('h'))
text
hash: listpack
hash-long: hashtable
hash-fields: 512 listpack
hash-fields: 513 hashtable

第二次写入超过默认 64 字节单值阈值,所以即使只有三个字段,也转换成哈希表。另两行用短字段名和短值验证了字段数边界。盲目调大阈值会延长紧凑结构的扫描与搬移时间,应该用真实字段分布测量。Hash 实现与字段过期扩展

HGETALL 会取回所有字段。一个 Hash 下放几百万条记录,虽然键数只有 1,返回量仍然巨大。大对象分批遍历用 HSCAN,但扫描不是一致性快照,期间的修改需要业务容忍;“分批”也不等于每批严格返回指定条数。SCAN 语义

List:两端队列很顺手,任务可靠性还要另外设计 ​

List 中的元素允许重复,按插入顺序排列。LPUSH/RPUSH 选择从哪一端加入,LPOP/RPOP 选择从哪一端取出;同侧压入弹出是栈,异侧是队列。LRANGE 的结束下标包含在结果里,-1 表示最后一个元素。

text
list: listpack a ['b', 'c']
list-long: quicklist

这里先 RPUSH a b c,再 LPOP,取出 a,剩下 b,c。随后添加一个 9000 字节的元素,编码从 listpack 变成 quicklist。quicklist 是串起多个节点的链表,常规节点内部仍用 listpack;特大元素还可以单独作为 plain 节点。因此“List 底层永远是双向链表”或“永远是 quicklist”都漏掉了版本与大小条件。t_list.c、quicklist.c

端点操作不需要从头找位置,中间下标和范围操作则可能遍历一段列表;LRANGE 0 -1 还要传输全部元素。选择它应基于两端访问为主,而不是把它当随机访问数组。

任务直接 LPOP 后,消费者若崩溃,任务已经从队列消失。LMOVE/BLMOVE 可以把任务原子移到处理中列表,成功后再移除,并由另外的逻辑找回超时任务。它提供的是可靠队列的基础动作,重试、重复处理和死信管理仍需自己实现。LMOVE

Set 与 ZSet:是否需要顺序,是关键区别 ​

Set 保存唯一成员,SADD 对已存在成员不会再添一份,SISMEMBER 判断存在,SINTER/SUNION/SDIFF 做集合运算。遍历顺序没有业务保证。纯整数成员可能用排序的紧凑 intset;短小字符串集合可以用 listpack;较大的集合转换成 hashtable。Set 源码

text
sets: intset listpack
set-long: hashtable

第一行分别来自 {1,2,3} 与 {red,blue}。给后者加入 65 字节成员后,变成 hashtable。Set 的常用成员查找在哈希表表示下平均 O(1),但集合交集、全量返回等命令仍有集合规模成本,不能把“Set O(1)”套到所有命令。

ZSet 在唯一成员之外维护浮点分数。修改同一成员的分数是更新位置,不是新增一个同名成员。ZADD 写分数,ZINCRBY 增加分数,ZRANGE 取排名或分数范围,ZRANK 取从零开始的排名。

python
call('ZADD', 'lab08:z', 10, 'b', 10, 'a', 20, 'c')
print('zset:', encoding('z'), call('ZRANGE', 'lab08:z', 0, -1))
text
zset: listpack ['a', 'b', 'c']
zset-long: skiplist

相同分数时按成员字节序排序,所以后写入的 a 在 b 前面。若需求是同分按时间先后,就要明确增加次序字段或设计可比较的成员,不能假设保留插入顺序。ZADD

大 ZSet 的 skiplist 编码实际还配合字典:字典按成员找到分数,跳表按分数与成员维护有序路径。插入、删除通常涉及 O(log N) 的有序结构工作,范围读取还要加返回 M 个成员的成本。给实验 ZSet 加一个 65 字节成员后便转换成此表示。ZSet 实现

分数是 double,不适合把任意 64 位订单号直接塞进去并期待逐整数精确。整数绝对值超过 2^53 后可能丢失相邻整数的区别。排行榜常用计分、延迟队列常用毫秒时间,仍要核对范围和比较规则。

Bitmap 与 HyperLogLog:都是省空间,但省掉的东西不同 ​

Bitmap 保存每个位置的一位 ​

给第 9 位设为 1:

python
call('SETBIT', 'lab08:bits', 9, 1)
print('bitmap:', call('TYPE', 'lab08:bits'),
      call('STRLEN', 'lab08:bits'), call('BITCOUNT', 'lab08:bits'))
text
bitmap: string 2 1

位偏移从零开始,第 9 位位于第二个字节,因此总长度为 2。它没有独立的键类型,TYPE 返回 string。对稠密用户编号做签到标记很合适;但如果编号是很大的随机数,SETBIT 需要把中间未使用字节也补出来。空间近似为 最大偏移/8,不是 实际签到人数/8。可以按日期与用户编号区间分片,或改用 Set。SETBIT

BITCOUNT 要扫描涉及的字节,BITOP 需要读取输入并生成结果。Bitmap 节省成员标识的空间,却不让扫描变成零成本。

HyperLogLog 只保留估算基数所需的摘要 ​

HLL 用哈希后位模式的统计特征估计不同元素数。PFADD 加入观测,PFCOUNT 估算人数,PFMERGE 合并多个观测集合的摘要。重复输入不会像简单计数器那样把人数一直加上去。

text
hll: string raw 2

实验输入 a,b,a,估算结果恰好是 2。这个小样本不能证明所有输入都精确。Redis 的实现使用 16384 个寄存器,稠密寄存器区约 12 KiB,外加头部及 Redis 对象开销,标准误差约 0.81%;它不是“每次最多错 0.81%”的硬边界。少量数据可用稀疏表示进一步省空间。8.2.2 HyperLogLog 实现

HLL 丢掉了成员本身,因此无法返回访客名单、精确删除某个访客,或者用它判断一个用户是否存在。需要这些能力就用精确集合或数据库索引。Bitmap 保留了每个位置的状态,HLL 则只保留整体统计信息,这是二者最重要的区别。

Stream:消息还在,与消息已确认,是两件事 ​

Stream 是带 ID 的追加式事件记录。XADD 写事件;XRANGE 按 ID 范围读;XREAD 从某个 ID 后继续读。消费者组把消费进度和待确认记录加在事件日志上,XREADGROUP 分配消息,XACK 确认处理完成。8.2.2 Stream 实现

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
sequenceDiagram
    participant P as 生产者
    participant S as Stream
    participant C as 消费者
    participant G as 组的待确认记录 PEL
    P->>S: XADD 1-0 event created
    C->>S: XREADGROUP GROUP workers c1 STREAMS key >
    S->>G: 记录 1-0 已分给 c1,等待确认
    S-->>C: 返回事件 1-0
    C->>C: 执行业务,可能失败或重复
    C->>S: XACK key workers 1-0
    S->>G: 移除待确认项
    Note over S: 原始事件仍在,需另定删除/裁剪策略
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
sequenceDiagram
    participant P as 生产者
    participant S as Stream
    participant C as 消费者
    participant G as 组的待确认记录 PEL
    P->>S: XADD 1-0 event created
    C->>S: XREADGROUP GROUP workers c1 STREAMS key >
    S->>G: 记录 1-0 已分给 c1,等待确认
    S-->>C: 返回事件 1-0
    C->>C: 执行业务,可能失败或重复
    C->>S: XACK key workers 1-0
    S->>G: 移除待确认项
    Note over S: 原始事件仍在,需另定删除/裁剪策略

实际创建一条事件、用消费者组领取,再确认:

text
stream: stream pending= 1
xack: 1 length= 1

pending=1 是待确认数;XACK 返回确认了 1 条,但 XLEN 仍为 1。确认不会删除事件,也不代表业务副作用在 Redis 外部恰好发生一次。消费者若执行成功但未确认便崩溃,后续认领可能重复执行业务,因此通常需要业务幂等键。XACK

内部 radix tree 根据消息 ID 组织节点,每个节点存一个 listpack,可以共享字段信息、紧凑存多条记录。消费者组、消费者和 PEL 另有状态,积压的待确认记录同样占内存。用 XPENDING 看未确认消息,用 XAUTOCLAIM 认领超过空闲阈值的消息;空闲不等于原消费者已经永久死去,重复执行仍要处理。

保留策略要同时回答“生产者最多写多少”和“慢消费者落后多久仍能补读”。MAXLEN ~ 允许近似裁剪,不能当成严格条数上限;裁掉消费者尚需的内容,会造成无法补读的缺口。8.2 还增加了与消费者引用有关的裁剪选项,采用前应按该版本 XTRIM 文档逐项选定,而不是随手套用最新版示例。XTRIM

Geo:先缩小候选区域,再算距离 ​

GEOADD 接收经度、纬度、成员名,写入一个 ZSet;score 保存坐标的空间编码。GEOSEARCH 按圆形或矩形搜索候选点,GEODIST 求两点距离。底层利用编码后的有序区间筛选候选,再做距离过滤,所以搜索区域很大、候选很多时仍然昂贵。geo.c

text
geo: zset listpack ['Palermo', 'Catania']

本次在 Palermo 周围 200 km 查找,并指定 ASC 按距离排序。TYPE 是 zset,小集合编码是 listpack,不应把 Geo 当成第六种独立键类型。删除位置使用 ZREM,但不要随意用普通 ZADD 改它的 score,否则会破坏空间编码的含义。

输入顺序是经度在前、纬度在后。Redis 支持的经纬度有范围限制,坐标来自不同坐标系时也应先转换。GEOSEARCH COUNT 限制返回条数,ANY 可以让实现更早停止寻找足够结果,但不能同时把结果理解成完整搜索后严格最近的前 N 个。GEOSEARCH、GEOADD

Geo 适合“附近有哪些店”,不负责路网距离、行车时间和多边形拓扑。把结果当成业务候选,再交给真正需要的地理或路线计算系统。

衡量内存与延迟时,要把边界算完整 ​

实验保存 1 字节内容:

text
memory: bytes= 1 usage= 40

STRLEN 只量内容长度;MEMORY USAGE 估算这个键和值关联的内存,含管理结构与分配开销。它不等于整个进程 RSS,更不能简单把所有键的值长相加当作容量计划。键名、字典桶、过期信息、客户端缓冲、复制积压和分配器碎片都会影响实际占用。MEMORY USAGE

本次 40 字节属于 libc 构建和这个键的实测值,换分配器、键长、版本可能变化。嵌套结构默认会采样估算,需要更完整估算可以考虑 SAMPLES 0,但大对象的遍历成本会增加。

落地时可以依次做三件事:按业务操作选类型;用典型和极端对象确认编码与命令复杂度;再测真实请求的返回字节数、尾延迟和进程内存。只跑一个小对象的 GET,无法说明批量返回、编码转换或巨型对象删除的代价。

依据与验证范围 ​

  • Redis 8.2.2 官方源码:object.c/db.c 的键值布局;t_hash.c、t_list.c、t_set.c、t_zset.c、t_stream.c、hyperloglog.c、geo.c 的具体实现;redis.conf 的默认阈值。正文各节已链接具体文件。
  • Redis 数据类型、内存优化:逻辑结构与小对象编码的整体关系。在线文档持续变化,本文内部编码以固定源码为准。
  • 命令边界:正文所链 SCAN、ZADD、SETBIT、LMOVE、XACK、XTRIM、GEOSEARCH、MEMORY USAGE 文档。
  • 实测:Redis 8.2.2,Linux/amd64,libc 分配器;Python 标准库 RESP2 客户端;临时实例关闭 RDB/AOF,只绑定回环地址。正文输出均来自下方脚本。
  • 未验证:生产负载性能、内存碎片长期变化、复制与集群故障、所有数据分布下的近似统计误差。本文没有把这些内容包装成实验结论。

完整复现实验 ​

需要 Python 3、Bash,以及可运行的 Redis 8.2.2 redis-server、redis-cli。以下脚本只应指向临时教学实例;启动包装器会为本次测试创建空目录,并在结束时关闭自己启动的进程、删除该临时目录。测试命令只会在自己启动的实例上执行,端口被占用则退出。启动时会读取目标端口的进程标识以核对归属。

可从官方源码包构建。该归档官方 SHA-256 为 4e340e8e822a82114b6fb0f7ca581b749fa876e31e36e9fbcb75416bec9d0608,出处为 redis-hashes。示例固定的是测试版本,生产环境应另外检查安全修复。

bash
curl -fLO https://download.redis.io/releases/redis-8.2.2.tar.gz
printf '%s  %s\n' 4e340e8e822a82114b6fb0f7ca581b749fa876e31e36e9fbcb75416bec9d0608 redis-8.2.2.tar.gz | sha256sum -c -
tar xzf redis-8.2.2.tar.gz
make -C redis-8.2.2 -j4 MALLOC=libc BUILD_TLS=no OPTIMIZATION=-O2 redis-server redis-cli
export REDIS_BIN="$PWD/redis-8.2.2/src"
"$REDIS_BIN/redis-server" --version
bash with-redis.sh python3 data.py

保存下面三个文件在同一目录。resp.py 是只供本例使用、按 UTF-8 解码的最小 RESP2 客户端,不是通用二进制安全或生产客户端。输出中的 PID 和临时目录每次不同。实验中没有压测,也不修改系统的内存或网络设置。

with-redis.sh ​

bash
#!/usr/bin/env bash
set -euo pipefail
: "${REDIS_BIN:?Set REDIS_BIN to the directory containing redis-server and redis-cli}"
export REDIS_PORT=${REDIS_PORT:-16389}
work=$(mktemp -d)
log=${REDIS_RUN_LOG:-$work/server.log}
pid=
cleanup() {
  if [[ -n "$pid" ]]; then
    kill -TERM "$pid" 2>/dev/null || true
    wait "$pid" 2>/dev/null || true
  fi
  rm -rf -- "$work"
}
trap cleanup EXIT
"$REDIS_BIN/redis-server" --bind 127.0.0.1 --port "$REDIS_PORT" \
  --protected-mode yes --save '' --appendonly no --dir "$work" \
  --daemonize no > "$log" 2>&1 &
pid=$!
echo "redis_pid=$pid port=$REDIS_PORT temporary_dir=$work" >&2
ready=0
for ((i=0;i<100;i++)); do
  kill -0 "$pid" 2>/dev/null || { cat "$log" >&2; exit 1; }
  if "$REDIS_BIN/redis-cli" -h 127.0.0.1 -p "$REDIS_PORT" INFO server 2>/dev/null | tr -d '\r' | grep -qx "process_id:$pid"; then
    ready=1; break
  fi
  sleep 0.05
done
[[ "$ready" == 1 ]] || { cat "$log" >&2; exit 1; }
"$@"

resp.py ​

代码块 · 37 行 · 点击收起 / 展开
python
import os
import socket

class RedisError(Exception):
    pass

class Redis:
    def __init__(self):
        self.sock = socket.create_connection(('127.0.0.1', int(os.environ.get('REDIS_PORT', '16389'))), timeout=3)
        self.f = self.sock.makefile('rb')
    def close(self):
        self.f.close()
        self.sock.close()
    def read(self):
        line = self.f.readline()
        if not line.endswith(b'\r\n'):
            raise EOFError('incomplete RESP line')
        kind, body = line[:1], line[1:-2]
        if kind == b'+': return body.decode()
        if kind == b'-': return RedisError(body.decode())
        if kind == b':': return int(body)
        if kind == b'$':
            n = int(body)
            if n == -1: return None
            data = self.f.read(n)
            if len(data) != n or self.f.read(2) != b'\r\n': raise EOFError('incomplete bulk')
            return data.decode()
        if kind == b'*':
            n = int(body)
            return None if n == -1 else [self.read() for _ in range(n)]
        raise ValueError('unsupported RESP type')
    def call(self, *args):
        vals = [str(a).encode() for a in args]
        wire = b'*%d\r\n' % len(vals)
        wire += b''.join(b'$%d\r\n' % len(v) + v + b'\r\n' for v in vals)
        self.sock.sendall(wire)
        return self.read()

data.py ​

代码块 · 55 行 · 点击收起 / 展开
python
from resp import Redis
r = Redis()
try:
    def call(*a):
        v = r.call(*a)
        if isinstance(v, Exception): raise v
        return v
    def encoding(k): return call('OBJECT', 'ENCODING', 'lab08:'+k)
    for key, value in [('n','42'),('s44','x'*44),('s45','x'*45)]:
        call('SET','lab08:'+key,value)
    print('string:',encoding('n'),encoding('s44'),encoding('s45'))
    for key, n, ttl in [('a',40,False),('b',41,False)]:
        args=['SET',key,'x'*n]
        if ttl: args += ['PX',60000]
        call(*args)
        print('string-boundary:',key,n,ttl,call('OBJECT','ENCODING',key))
    call('APPEND','lab08:s44','y')
    print('append:',encoding('s44'))
    call('HSET','lab08:h','name','Ada','age','37')
    print('hash:',encoding('h'))
    call('HSET','lab08:h','bio','x'*65)
    print('hash-long:',encoding('h'))
    for n in [512,513]:
        k='lab08:h'+str(n)
        args=sum(([str(i),'v'] for i in range(n)),[])
        call('HSET',k,*args)
        print('hash-fields:',n,call('OBJECT','ENCODING',k))
    call('RPUSH','lab08:l','a','b','c')
    print('list:',encoding('l'),call('LPOP','lab08:l'),call('LRANGE','lab08:l',0,-1))
    call('RPUSH','lab08:l','x'*9000)
    print('list-long:',encoding('l'))
    call('SADD','lab08:ints',1,2,3)
    call('SADD','lab08:words','red','blue')
    print('sets:',encoding('ints'),encoding('words'))
    call('SADD','lab08:words','x'*65)
    print('set-long:',encoding('words'))
    call('ZADD','lab08:z',10,'b',10,'a',20,'c')
    print('zset:',encoding('z'),call('ZRANGE','lab08:z',0,-1))
    call('ZADD','lab08:z',30,'x'*65)
    print('zset-long:',encoding('z'))
    call('SETBIT','lab08:bits',9,1)
    print('bitmap:',call('TYPE','lab08:bits'),call('STRLEN','lab08:bits'),call('BITCOUNT','lab08:bits'))
    call('PFADD','lab08:hll','a','b','a')
    print('hll:',call('TYPE','lab08:hll'),encoding('hll'),call('PFCOUNT','lab08:hll'))
    call('XADD','lab08:stream','1-0','event','created')
    call('XGROUP','CREATE','lab08:stream','workers','0')
    call('XREADGROUP','GROUP','workers','c1','COUNT',1,'STREAMS','lab08:stream','>')
    print('stream:',encoding('stream'),'pending=',call('XPENDING','lab08:stream','workers')[0])
    print('xack:',call('XACK','lab08:stream','workers','1-0'),'length=',call('XLEN','lab08:stream'))
    call('GEOADD','lab08:geo',13.361389,38.115556,'Palermo',15.087269,37.502669,'Catania')
    print('geo:',call('TYPE','lab08:geo'),encoding('geo'),call('GEOSEARCH','lab08:geo','FROMMEMBER','Palermo','BYRADIUS',200,'km','ASC'))
    call('SET','lab08:tiny','x')
    print('memory: bytes=',call('STRLEN','lab08:tiny'),'usage=',call('MEMORY','USAGE','lab08:tiny'))
finally:
    r.close()

保留各书来源与许可 · 示例运行条件以正文为准